Understanding Model-View-ViewModel (MVVM) Architecture in iOS
Course: iOS App Development Training
Topic: Understanding Model-View-ViewModel Architecture in iOS
Course Link: iOS Training Course
Demo Registration: Register for a Course Demo
Website: JustAcademy Official Website
1. Introduction to MVVM Architecture
Model-View-ViewModel (MVVM) is a software architectural pattern used to organize iOS application code into separate responsibilities. It separates application data, presentation logic, and user interface code, making applications easier to understand, maintain, test, and extend.
In a small application, it may be convenient to write network requests, data processing, validation, and UI updates inside a single view controller. However, as an application grows, this approach can lead to large classes, duplicated logic, difficult debugging, and tightly coupled components.
MVVM addresses these challenges by dividing an application into three main components: Model, View, and ViewModel. In larger projects, services and repositories can also be added to handle networking, databases, and other data sources.
2. Learning Objectives
- Understand the concept and purpose of MVVM architecture.
- Learn the responsibilities of Model, View, and ViewModel.
- Understand how data flows between MVVM components.
- Implement MVVM using Swift and SwiftUI.
- Apply MVVM to UIKit applications.
- Understand observable properties and SwiftUI state management.
- Integrate API services with a ViewModel.
- Separate business logic from presentation logic.
- Learn dependency injection and unit testing fundamentals.
- Identify common MVVM mistakes and best practices.
3. What Is MVVM?
MVVM stands for Model-View-ViewModel. It is an architectural pattern that helps developers separate the information an application manages from the way that information is presented to users.
Model
The Model represents application data and domain concepts. Examples include student records, product information, user profiles, orders, and articles.
View
The View displays information and receives user interactions. In iOS development, it may be a SwiftUI View or a UIKit interface containing UIView, UILabel, UIButton, and other UI components.
ViewModel
The ViewModel manages presentation state and presentation-related logic. It prepares information for display, validates user input, responds to actions, and coordinates calls to services or repositories.
4. MVVM Architecture Diagram
User Interaction
|
v
+-----------+
| View |
| SwiftUI / |
| UIKit |
+-----------+
|
User Actions
|
v
+-----------+
| ViewModel |
| UI State |
| Validation|
+-----------+
|
Service Calls
|
v
+-----------+
| Service |
| Repository|
+-----------+
|
v
+-----------+
| Model |
| App Data |
+-----------+
ViewModel publishes presentation state
|
v
View Updates
This is a common implementation rather than a mandatory structure. Depending on the application, a ViewModel may access a model directly or communicate through services, repositories, and domain-level components.
5. Understanding the Model Component
The Model represents the data and concepts used by an application. It should not need to know how a particular screen displays its information.
Responsibilities of the Model
- Represent application entities.
- Define stored properties and data relationships.
- Support data decoding and encoding when required.
- Represent values retrieved from APIs or databases.
- Contain appropriate domain rules when they belong to the data or domain layer.
Example: Student Model
import Foundation
struct Student: Identifiable {
let id: Int
var name: String
var course: String
var email: String
}
The Student structure represents a student record. It does not create labels, display buttons, or manage screen navigation.
Example Usage
let student = Student(
id: 101,
name: "Aarav Sharma",
course: "iOS Development",
email: "[email protected]"
)
print(student.name)
print(student.course)
6. Understanding the View Component
The View is responsible for presenting information and receiving user input. It should focus primarily on the interface rather than performing every application operation itself.
Examples of Views
- Login and registration screens.
- Student profile screens.
- Product listing pages.
- Shopping cart screens.
- Dashboard and analytics screens.
- Forms containing text fields and validation messages.
Example: Student View in SwiftUI
import SwiftUI
struct StudentView: View {
let student: Student
var body: some View {
VStack(spacing: 12) {
Text(student.name)
.font(.title)
Text(student.course)
.font(.headline)
Text(student.email)
.font(.subheadline)
}
.padding()
}
}
This View receives a Student value and displays its properties. It does not need to know whether the student information came from a local database, an API, or a test fixture.
7. Understanding the ViewModel Component
The ViewModel connects presentation logic with the application's data and services. It exposes information that the View needs and provides methods for handling user actions.
Responsibilities of a ViewModel
- Maintain presentation state.
- Prepare data for display.
- Validate form inputs.
- Coordinate service or repository calls.
- Manage loading, success, empty, and error states.
- Provide actions that the View can invoke.
- Help keep UI code simple and testable.
Example: Student ViewModel
import Foundation
import Combine
@MainActor
final class StudentViewModel: ObservableObject {
@Published private(set) var student: Student
init(student: Student) {
self.student = student
}
var studentName: String {
student.name
}
var courseName: String {
student.course
}
func updateCourse(_ newCourse: String) {
student.course = newCourse
}
}
The ViewModel provides display-ready properties and an action for updating the course. The @Published property notifies compatible observers when the student value changes.
8. How Data Flows Through MVVM
MVVM commonly uses a unidirectional flow of user actions and state updates. The View sends actions to the ViewModel, and the ViewModel updates observable state that the View displays.
- The View displays the current presentation state.
- The user performs an action, such as tapping a button.
- The View invokes a ViewModel method.
- The ViewModel validates the action or requests data from a service.
- The service returns a result or an error.
- The ViewModel updates its presentation state.
- The View observes the changed state and updates the interface.
This approach reduces direct dependencies between UI components and data operations. It also allows developers to test presentation behavior without rendering the entire screen.
9. Implementing MVVM Using SwiftUI
The following example combines a Model, a ViewModel, and a View into a simple student profile feature.
Step 1: Create the Model
import Foundation
struct Student {
let name: String
var course: String
}
Step 2: Create the ViewModel
import Foundation
import Combine
@MainActor
final class StudentViewModel: ObservableObject {
@Published private(set) var student: Student
init(student: Student) {
self.student = student
}
func updateCourse() {
student.course = "Advanced Swift"
}
}
Step 3: Create the View
import SwiftUI
struct StudentProfileView: View {
@StateObject private var viewModel = StudentViewModel(
student: Student(
name: "Aarav",
course: "iOS Development"
)
)
var body: some View {
VStack(spacing: 16) {
Text(viewModel.student.name)
.font(.title)
Text(viewModel.student.course)
.font(.headline)
Button("Update Course") {
viewModel.updateCourse()
}
}
.padding()
}
}
Explanation
- Student represents the data.
- StudentViewModel manages the student information and exposes an update action.
@Published makes changes observable to compatible observers.
@StateObject creates and retains the ObservableObject owned by the View.
- The View displays the student's current name and course.
- When the course changes, SwiftUI updates the relevant displayed information.
This implementation uses the traditional ObservableObject approach. It is suitable for projects using Combine and compatible SwiftUI deployment targets.
10. Using the Modern Observation Framework
For applications targeting iOS 17 or later, Swift's Observation framework provides the @Observable macro. It enables SwiftUI to track which observable properties a View reads and update the relevant interface when those properties change.
Example ViewModel
import Foundation
import Observation
@Observable
@MainActor
final class CounterViewModel {
private(set) var count = 0
func increment() {
count += 1
}
func reset() {
count = 0
}
}
Example View
import SwiftUI
struct CounterView: View {
@State private var viewModel = CounterViewModel()
var body: some View {
VStack(spacing: 16) {
Text("Count: \(viewModel.count)")
.font(.title)
Button("Increment") {
viewModel.increment()
}
Button("Reset") {
viewModel.reset()
}
}
.padding()
}
}
The View owns the observable ViewModel using @State. SwiftUI tracks the observable properties used by the View and updates the displayed count when it changes.
Compatibility note: The Observation-based approach is available in SwiftUI on iOS 17 and later. For applications supporting older versions, ObservableObject, @Published, @StateObject, and @ObservedObject remain useful options.
Reference: Apple Documentation: Managing Model Data in Your App.
11. MVVM with User Input and Validation
Forms are a common use case for MVVM. Input validation can be handled by the ViewModel, while the View focuses on text fields, buttons, and displaying messages.
Example: Login ViewModel
import Foundation
import Combine
@MainActor
final class LoginViewModel: ObservableObject {
@Published var email = ""
@Published var password = ""
var isFormValid: Bool {
email.contains("@") && password.count >= 8
}
var validationMessage: String {
if email.isEmpty {
return "Email is required"
}
if !email.contains("@") {
return "Enter a valid email address"
}
if password.count < 8 {
return "Password must contain at least 8 characters"
}
return "Form is valid"
}
}
Example: Login View
import SwiftUI
struct LoginView: View {
@StateObject private var viewModel = LoginViewModel()
var body: some View {
VStack(spacing: 16) {
TextField("Email", text: $viewModel.email)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.textFieldStyle(.roundedBorder)
SecureField("Password", text: $viewModel.password)
.textFieldStyle(.roundedBorder)
Text(viewModel.validationMessage)
.foregroundStyle(
viewModel.isFormValid ? .green : .red
)
Button("Login") {
// Call the authentication service here.
}
.disabled(!viewModel.isFormValid)
}
.padding()
}
}
In this example, the ViewModel contains basic validation rules. A production authentication feature should also validate credentials securely on the server, handle authentication failures, and avoid logging sensitive information.
12. MVVM with API Integration
In a data-driven iOS application, the ViewModel often coordinates API requests and exposes the resulting state to the View. A dedicated service keeps networking implementation separate from presentation logic.
Step 1: Define the Model
import Foundation
struct Article: Decodable, Identifiable {
let id: Int
let title: String
let body: String
}
Step 2: Create the API Service
import Foundation
struct ArticleService {
func fetchArticles() async throws -> [Article] {
guard let url = URL(
string: "https://jsonplaceholder.typicode.com/posts"
) else {
throw URLError(.badURL)
}
let (data, response) =
try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse,
(200...299).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode([Article].self, from: data)
}
}
Step 3: Create the ViewModel
import Foundation
import Combine
@MainActor
final class ArticleViewModel: ObservableObject {
@Published private(set) var articles: [Article] = []
@Published private(set) var isLoading = false
@Published private(set) var errorMessage: String?
private let service: ArticleService
init(service: ArticleService = ArticleService()) {
self.service = service
}
func loadArticles() async {
guard !isLoading else { return }
isLoading = true
errorMessage = nil
defer {
isLoading = false
}
do {
articles = try await service.fetchArticles()
} catch is CancellationError {
// Cancellation is not displayed as an ordinary error.
} catch {
errorMessage = error.localizedDescription
}
}
}
Step 4: Create the View
import SwiftUI
struct ArticleListView: View {
@StateObject private var viewModel = ArticleViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading && viewModel.articles.isEmpty {
ProgressView("Loading articles...")
} else if let error = viewModel.errorMessage,
viewModel.articles.isEmpty {
VStack(spacing: 12) {
Text(error)
Button("Retry") {
Task {
await viewModel.loadArticles()
}
}
}
} else {
List(viewModel.articles) { article in
VStack(alignment: .leading, spacing: 8) {
Text(article.title)
.font(.headline)
Text(article.body)
.font(.subheadline)
}
}
.overlay {
if viewModel.isLoading {
ProgressView()
}
}
}
}
.navigationTitle("Articles")
.task {
await viewModel.loadArticles()
}
}
}
}
Understanding the Responsibilities
- Article: Represents an article returned by the API.
- ArticleService: Performs the network request and decodes the response.
- ArticleViewModel: Manages the articles, loading state, and error state.
- ArticleListView: Displays the information and provides retry and loading feedback.
Production considerations: The example keeps networking separate, but large responses may require moving expensive decoding away from the main actor. A real application should also consider cancellation, stale responses, request retries, and more specific error messages. The loading guard shown here prevents overlapping requests but may need a different policy for refresh or search features.
13. Understanding MVVM with UIKit
MVVM is not limited to SwiftUI. UIKit applications can use UIViewController as the View layer and a ViewModel to manage presentation state.
Example ViewModel
import Foundation
import Combine
@MainActor
final class WelcomeViewModel: ObservableObject {
@Published private(set) var message = "Welcome to iOS"
func updateMessage() {
message = "Welcome to MVVM Architecture"
}
}
Example View Controller
import UIKit
import Combine
final class WelcomeViewController: UIViewController {
private let viewModel = WelcomeViewModel()
private var cancellables = Set<AnyCancellable>()
private let messageLabel = UILabel()
private let updateButton = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupViews()
bindViewModel()
}
private func setupViews() {
messageLabel.textAlignment = .center
updateButton.setTitle("Update Message", for: .normal)
updateButton.addTarget(
self,
action: #selector(updateMessage),
for: .touchUpInside
)
let stack = UIStackView(
arrangedSubviews: [messageLabel, updateButton]
)
stack.axis = .vertical
stack.spacing = 20
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(
equalTo: view.centerXAnchor
),
stack.centerYAnchor.constraint(
equalTo: view.centerYAnchor
),
stack.leadingAnchor.constraint(
greaterThanOrEqualTo: view.leadingAnchor,
constant: 20
),
stack.trailingAnchor.constraint(
lessThanOrEqualTo: view.trailingAnchor,
constant: -20
)
])
}
private func bindViewModel() {
viewModel.$message
.sink { [weak self] message in
self?.messageLabel.text = message
}
.store(in: &cancellables)
}
@objc private func updateMessage() {
viewModel.updateMessage()
}
}
The ViewController observes changes from the ViewModel through Combine and updates the label. Because the ViewModel is isolated to the main actor, the published state is UI-oriented. In more complex applications, explicit binding strategies and lifecycle management should be considered.
14. MVVM and Dependency Injection
Dependency injection means providing a component with the services or objects it needs rather than creating every dependency internally. It improves testability and makes it easier to replace real services with mock implementations.
Example Service Protocol
import Foundation
protocol GreetingServiceProtocol {
func fetchGreeting() async throws -> String
}
struct GreetingService: GreetingServiceProtocol {
func fetchGreeting() async throws -> String {
return "Hello from the service"
}
}
Example ViewModel Using Injection
import Foundation
import Combine
@MainActor
final class GreetingViewModel: ObservableObject {
@Published private(set) var greeting = ""
private let service: any GreetingServiceProtocol
init(service: any GreetingServiceProtocol) {
self.service = service
}
func loadGreeting() async {
do {
greeting = try await service.fetchGreeting()
} catch {
greeting = "Unable to load greeting"
}
}
}
The ViewModel depends on a protocol rather than a specific service implementation. A test can inject a mock service to return predictable results without making a real network request.
15. Unit Testing a ViewModel
Unit testing verifies the behavior of a component independently from the complete UI. ViewModels are often suitable for unit testing because they expose methods and state that can be checked directly.
Example Counter ViewModel
import Foundation
@MainActor
final class CounterTestViewModel {
private(set) var count = 0
func increment() {
count += 1
}
func reset() {
count = 0
}
}
Example XCTest
import XCTest
@testable import YourAppModule
final class CounterTestViewModelTests: XCTestCase {
@MainActor
func testIncrementIncreasesCount() {
let viewModel = CounterTestViewModel()
viewModel.increment()
XCTAssertEqual(viewModel.count, 1)
}
@MainActor
func testResetSetsCountToZero() {
let viewModel = CounterTestViewModel()
viewModel.increment()
viewModel.reset()
XCTAssertEqual(viewModel.count, 0)
}
}
Replace YourAppModule with the actual module name of the application. These tests confirm that the ViewModel increments and resets its state correctly without requiring a rendered screen.
16. Advantages of MVVM Architecture
| Advantage |
Description |
| Separation of concerns |
Data, presentation logic, and UI code have distinct responsibilities. |
| Maintainability |
Changes can often be made to one component without rewriting the entire feature. |
| Testability |
ViewModel behavior can be tested independently of the UI. |
| Reusability |
Presentation logic may be reused by multiple views where appropriate. |
| Readability |
Code is easier to understand when responsibilities are clearly defined. |
| Scalability |
Services and repositories can be added as application requirements grow. |
| State management |
Observable state can keep the interface synchronized with application data. |
17. Limitations of MVVM
- It can introduce unnecessary complexity in very small applications.
- ViewModels can become too large if responsibilities are not separated.
- Observable state and data flow need careful design.
- Developers need to understand state ownership and dependency injection.
- MVVM alone does not guarantee good performance or clean code.
- Complex navigation and coordination may require additional architectural components.
Choose the level of architecture that matches the application. A small screen may need a simple ViewModel, while a large feature may benefit from separate services, repositories, and domain components.
18. MVVM vs. MVC
| Feature |
MVC |
MVVM |
| Full form |
Model-View-Controller |
Model-View-ViewModel |
| Presentation logic |
Often managed by the Controller. |
Primarily managed by the ViewModel. |
| UI relationship |
The Controller coordinates the Model and View. |
The View observes ViewModel state and forwards user actions. |
| Testing |
Controllers can be difficult to test when responsibilities are mixed. |
ViewModel logic can often be tested independently. |
| Typical iOS use |
Common in UIKit applications. |
Common in SwiftUI and UIKit applications. |
| Complexity |
Can be straightforward for smaller features. |
Adds a dedicated presentation layer. |
19. Common MVVM Mistakes
| Mistake |
Problem |
Solution |
| Putting all logic in the View |
The UI becomes difficult to maintain. |
Move presentation logic into the ViewModel. |
| Creating an oversized ViewModel |
Responsibilities become mixed together. |
Extract services or separate feature logic. |
| Making API calls directly in every View |
Networking logic becomes duplicated. |
Use a shared service or repository where appropriate. |
| Ignoring loading and error states |
Users receive little feedback when operations fail. |
Model loading, success, empty, and failure states. |
| Using incorrect state ownership |
Updates may not be observed as expected. |
Choose suitable SwiftUI state wrappers or Observation patterns. |
| Skipping unit tests |
Presentation logic errors can be harder to isolate. |
Test validation, state changes, and error handling. |
| Blocking the main thread |
The interface may freeze during expensive work. |
Use asynchronous APIs and appropriate execution contexts. |
20. MVVM Best Practices
- Keep Views focused on layout, display, and user interaction.
- Keep presentation state and presentation logic in the ViewModel.
- Use services or repositories for networking and persistence when appropriate.
- Use dependency injection to make dependencies replaceable during testing.
- Keep ViewModels focused on a screen or feature.
- Use MainActor isolation for UI-facing state where appropriate.
- Handle loading, empty, success, and error states explicitly.
- Keep expensive synchronous work off the main actor.
- Test business and presentation logic independently where practical.
- Choose the Observation or ObservableObject approach based on deployment targets and project requirements.
- Avoid introducing abstractions that do not solve a real problem.
21. Practice Exercises
- Create a Student Model and a ViewModel that displays student information.
- Build a counter application using MVVM and SwiftUI.
- Create a login screen with email and password validation.
- Build a product list using a Model, ViewModel, API service, and View.
- Add loading, empty, success, and error states to an API-driven screen.
- Build a profile screen using UIKit and Combine.
- Write unit tests for validation and ViewModel state changes.
- Inject a mock service into a ViewModel and test its success and failure cases.
- Refactor a large ViewController by moving presentation logic into a ViewModel.
22. Interview Questions and Answers
Q1. What is MVVM architecture?
MVVM is an architectural pattern that separates application data, user interface, and presentation logic into Model, View, and ViewModel components.
Q2. What is the responsibility of the Model?
The Model represents application data and domain concepts, such as users, products, articles, and orders.
Q3. What is the responsibility of the View?
The View displays information and receives user interactions through SwiftUI or UIKit components.
Q4. What is the role of the ViewModel?
The ViewModel manages presentation state, prepares data for display, and coordinates actions and service calls.
Q5. Why is MVVM useful in SwiftUI?
SwiftUI can observe state and update the interface when relevant observable properties change. MVVM helps keep presentation logic separate from the View's layout code.
Q6. What is the difference between @StateObject and @ObservedObject?
In the traditional ObservableObject approach, @StateObject is generally used when a View owns and creates an observable object. @ObservedObject is used when a View receives an existing observable object from elsewhere.
Q7. What is the purpose of @Published?
@Published publishes changes to a property of an ObservableObject so compatible observers can respond to updates.
Q8. Can MVVM be used with UIKit?
Yes. A UIViewController can act as the View layer and observe a ViewModel through Combine, callbacks, delegates, or another binding mechanism.
Q9. Should API calls be written directly inside the ViewModel?
Small applications may keep simple requests in a ViewModel, but a dedicated service or repository often improves reuse, separation, and testability in larger applications.
Q10. How do you test a ViewModel?
Instantiate the ViewModel, call its methods, and assert the resulting state. Inject mock services to test asynchronous success and failure scenarios.
Q11. What is dependency injection?
Dependency injection means providing a component with the services or objects it needs, rather than having it create all dependencies internally.
Q12. What is the difference between ObservableObject and @Observable?
ObservableObject works with Combine-based observation and property wrappers such as @Published. The @Observable macro uses the Observation framework to track property access and changes, and is supported by SwiftUI on iOS 17 and later.
23. Quick Revision Checklist
- MVVM stands for Model-View-ViewModel.
- Model represents data and domain concepts.
- View displays information and receives user actions.
- ViewModel manages presentation state and related logic.
- Services or repositories can handle external data access.
- SwiftUI supports observable state through Observation and ObservableObject.
- UIKit can use MVVM with Combine or other binding approaches.
- Dependency injection improves testability.
- Loading, empty, success, and error states should be handled.
- ViewModels can be tested independently of the rendered UI.
- Keep expensive synchronous work off the main actor.
24. Conclusion
Model-View-ViewModel architecture provides a practical way to organize iOS applications by separating data, presentation logic, and user interface responsibilities. It helps developers build applications that are easier to maintain, test, and extend as new features are introduced.
By combining MVVM with SwiftUI, UIKit, observable state, asynchronous programming, dependency injection, and unit testing, developers can create reliable and maintainable iOS applications while keeping their code organized.
Related Learning Resources